/**
 * 
 */
package org.swing.utility.jai.support;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.stream.MemoryCacheImageInputStream;
import javax.imageio.stream.MemoryCacheImageOutputStream;
import javax.media.jai.PlanarImage;

import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Transparency;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.ComponentColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferDouble;
import java.awt.image.DataBufferFloat;
import java.awt.image.DataBufferInt;
import java.awt.image.DataBufferShort;
import java.awt.image.DataBufferUShort;
import java.awt.image.IndexColorModel;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Vector;


/**
 * @author  Le Quynh Nhu
 *
 */
public class ImageUtils {

    /**
     * Converts the given rendered image into an image of the given {#link java.awt.image.BufferedImage} type.
     *
     * @param image     the source image
     * @param imageType the  {#link java.awt.image.BufferedImage} type
     * @return the buffered image of the given type
     */
    public static BufferedImage convertImage(RenderedImage image, int imageType) {
        final BufferedImage newImage;
        final int width = image.getWidth();
        final int height = image.getHeight();
        if (imageType != BufferedImage.TYPE_CUSTOM) {
            newImage = new BufferedImage(width, height, imageType);
        } else {
            // create custom image
            final ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
            final ColorModel cm = new ComponentColorModel(cs, false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE);
            final WritableRaster wr = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, width, height, 3 * width, 3,
                                                                     new int[]{2, 1, 0}, null);
            newImage = new BufferedImage(cm, wr, false, null);
        }
        final Graphics2D graphics = newImage.createGraphics();
        graphics.drawRenderedImage(image, null);
        graphics.dispose();
        return newImage;
    }

    /**
     * Returns an array containing the minimum and maximum value of the native data type used to store pixel values in
     * the given image.
     *
     * @param dataType a data type as defined in <code>DataBuffer</code>
     * @see java.awt.image.DataBuffer
     */
    public static double[] getDataTypeMinMax(int dataType, double[] minmax) {
        if (minmax == null) {
            minmax = new double[2];
        }
        if (dataType == DataBuffer.TYPE_BYTE
                || dataType == DataBuffer.TYPE_INT) {
            minmax[0] = 0.0;
            minmax[1] = 255.0;
        } else if (dataType == DataBuffer.TYPE_SHORT) {
            minmax[0] = Short.MIN_VALUE;
            minmax[1] = Short.MAX_VALUE;
        } else if (dataType == DataBuffer.TYPE_USHORT) {
            minmax[0] = 0.0;
            minmax[1] = 2.0 * Short.MAX_VALUE - 1.0;
        } else {
            minmax[0] = 0.0;
            minmax[1] = 1.0;
        }
        return minmax;
    }

    /**
     * Gets a textual representation of the supplied raster data type
     *
     * @param dataType a data type as defined in <code>DataBuffer</code>
     * @return a textual representation of the supplied raster data type
     * @see java.awt.image.DataBuffer
     */
    public static String getDataTypeName(int dataType) {
        switch (dataType) {
            case DataBuffer.TYPE_BYTE:
                return UtilConstants.BUFFER_BYTE_NAME;
            case DataBuffer.TYPE_SHORT:
                return UtilConstants.BUFFER_SHORT_NAME;
            case DataBuffer.TYPE_USHORT:
                return UtilConstants.BUFFER_USHORT_NAME;
            case DataBuffer.TYPE_INT:
                return UtilConstants.BUFFER_INT_NAME;
            case DataBuffer.TYPE_FLOAT:
                return UtilConstants.BUFFER_FLOAT_NAME;
            case DataBuffer.TYPE_DOUBLE:
                return UtilConstants.BUFFER_DOUBLE_NAME;
            case DataBuffer.TYPE_UNDEFINED:
                return UtilConstants.BUFFER_UNDEFINED_NAME;
            default:
                return UtilConstants.BUFFER_UNKNOWN_NAME;
        }
    }

    /**
     * Gets a textual representation of the supplied color space type
     *
     * @param spaceType a dcolor space type as defined in <code>ColorSpace</code>
     * @return a textual representation of the color space
     * @see java.awt.color.ColorSpace
     */
    public static String getColorSpaceName(int spaceType) {
        switch (spaceType) {
            case ColorSpace.TYPE_XYZ:
                return UtilConstants.CS_TYPE_XYZ;
            case ColorSpace.TYPE_Lab:
                return UtilConstants.CS_TYPE_LAB;
            case ColorSpace.TYPE_Luv:
                return UtilConstants.CS_TYPE_LUV;
            case ColorSpace.TYPE_YCbCr:
                return UtilConstants.CS_TYPE_YCBCR;
            case ColorSpace.TYPE_Yxy:
                return UtilConstants.CS_TYPE_YXY;
            case ColorSpace.TYPE_RGB:
                return UtilConstants.CS_TYPE_RGB;
            case ColorSpace.TYPE_GRAY:
                return UtilConstants.CS_TYPE_GRAY;
            case ColorSpace.TYPE_HSV:
                return UtilConstants.CS_TYPE_HSV;
            case ColorSpace.TYPE_HLS:
                return UtilConstants.CS_TYPE_HLS;
            case ColorSpace.TYPE_CMYK:
                return UtilConstants.CS_TYPE_CMYK;
            case ColorSpace.TYPE_CMY:
                return UtilConstants.CS_TYPE_CMY;
            case ColorSpace.TYPE_2CLR:
                return UtilConstants.CS_TYPE_2CLR;
            case ColorSpace.TYPE_3CLR:
                return UtilConstants.CS_TYPE_3CLR;
            case ColorSpace.TYPE_4CLR:
                return UtilConstants.CS_TYPE_4CLR;
            case ColorSpace.TYPE_5CLR:
                return UtilConstants.CS_TYPE_5CLR;
            case ColorSpace.TYPE_6CLR:
                return UtilConstants.CS_TYPE_6CLR;
            case ColorSpace.TYPE_7CLR:
                return UtilConstants.CS_TYPE_7CLR;
            case ColorSpace.TYPE_8CLR:
                return UtilConstants.CS_TYPE_8CLR;
            case ColorSpace.TYPE_9CLR:
                return UtilConstants.CS_TYPE_9CLR;
            case ColorSpace.TYPE_ACLR:
                return UtilConstants.CS_TYPE_ACLR;
            case ColorSpace.TYPE_BCLR:
                return UtilConstants.CS_TYPE_BCLR;
            case ColorSpace.TYPE_CCLR:
                return UtilConstants.CS_TYPE_CCLR;
            case ColorSpace.TYPE_DCLR:
                return UtilConstants.CS_TYPE_DCLR;
            case ColorSpace.TYPE_ECLR:
                return UtilConstants.CS_TYPE_ECLR;
            case ColorSpace.TYPE_FCLR:
                return UtilConstants.CS_TYPE_FCLR;
            default:
                return UtilConstants.CS_TYPE_UNKNOWN;
        }
    }


    public static BufferedImage createGreyscaleColorModelImage(int width, int height, byte[] data) {
        ColorModel cm = create8BitGreyscaleColorModel();
        DataBufferByte db = new DataBufferByte(data, data.length);
        WritableRaster wr = WritableRaster.createBandedRaster(db, width, height, width, new int[]{0}, new int[]{0},
                                                              null);
        return new BufferedImage(cm, wr, false, null);
    }

    public static BufferedImage createIndexedImage(int width, int height, byte[] data, IndexColorModel cm) {
        final int numSamples = data.length;
        SampleModel sm = cm.createCompatibleSampleModel(width, height);
        DataBuffer db = new DataBufferByte(data, numSamples);
        WritableRaster wr = WritableRaster.createWritableRaster(sm, db, null);
        return new BufferedImage(cm, wr, false, null);
    }

    public static ColorModel create8BitGreyscaleColorModel() {
        final ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_GRAY);
        return new ComponentColorModel(cs, // colorSpace
                                       new int[]{8}, // bits
                                       false, // hasAlpha
                                       false, // isAlphaPremultiplied
                                       Transparency.OPAQUE, // transparency
                                       DataBuffer.TYPE_BYTE);
    }

    public static Object getPrimitiveArray(DataBuffer dataBuffer) {
        switch (dataBuffer.getDataType()) {
            case DataBuffer.TYPE_BYTE:
                return ((DataBufferByte) dataBuffer).getData();
            case DataBuffer.TYPE_SHORT:
                return ((DataBufferShort) dataBuffer).getData();
            case DataBuffer.TYPE_USHORT:
                return ((DataBufferUShort) dataBuffer).getData();
            case DataBuffer.TYPE_INT:
                return ((DataBufferInt) dataBuffer).getData();
            case DataBuffer.TYPE_FLOAT:
                return ((DataBufferFloat) dataBuffer).getData();
            case DataBuffer.TYPE_DOUBLE:
                return ((DataBufferDouble) dataBuffer).getData();
            default:
                throw new IllegalArgumentException("dataBuffer");
        }
    }

    public static Object createDataBufferArray(int dataBufferType, int size) {
        switch (dataBufferType) {
            case DataBuffer.TYPE_BYTE:
                return new byte[size];
            case DataBuffer.TYPE_SHORT:
            case DataBuffer.TYPE_USHORT:
                return new short[size];
            case DataBuffer.TYPE_INT:
                return new int[size];
            case DataBuffer.TYPE_FLOAT:
                return new float[size];
            case DataBuffer.TYPE_DOUBLE:
                return new double[size];
            default:
                throw new IllegalArgumentException("dataBuffer");
        }
    }
    
    static public String imageTypeName(BufferedImage img) {
		switch (img.getType()) {
		case BufferedImage.TYPE_3BYTE_BGR:
			return "TYPE_3BYTE_BGR";
		case BufferedImage.TYPE_4BYTE_ABGR:
			return "TYPE_4BYTE_ABGR";
		case BufferedImage.TYPE_4BYTE_ABGR_PRE:
			return "TYPE_4BYTE_ABGR_PRE";
		case BufferedImage.TYPE_BYTE_BINARY:
			return "TYPE_BYTE_BINARY";
		case BufferedImage.TYPE_BYTE_GRAY:
			return "TYPE_BYTE_GRAY";
		case BufferedImage.TYPE_BYTE_INDEXED:
			return "TYPE_BYTE_INDEXED";
		case BufferedImage.TYPE_CUSTOM:
			return "TYPE_CUSTOM";
		case BufferedImage.TYPE_INT_ARGB:
			return "TYPE_INT_ARGB";
		case BufferedImage.TYPE_INT_ARGB_PRE:
			return "TYPE_INT_ARGB_PRE";
		case BufferedImage.TYPE_INT_BGR:
			return "TYPE_INT_BGR";
		case BufferedImage.TYPE_INT_RGB:
			return "TYPE_INT_RGB";
		case BufferedImage.TYPE_USHORT_555_RGB:
			return "TYPE_USHORT_555_RGB";
		case BufferedImage.TYPE_USHORT_565_RGB:
			return "TYPE_USHORT_565_RGB";
		case BufferedImage.TYPE_USHORT_GRAY:
			return "TYPE_USHORT_GRAY";
		}
		return "unknown image type #" + img.getType();
	}

	static public int nrChannels(BufferedImage img) {
		switch (img.getType()) {
		case BufferedImage.TYPE_3BYTE_BGR:
			return 3;
		case BufferedImage.TYPE_4BYTE_ABGR:
			return 4;
		case BufferedImage.TYPE_BYTE_GRAY:
			return 1;
		case BufferedImage.TYPE_INT_BGR:
			return 3;
		case BufferedImage.TYPE_INT_ARGB:
			return 4;
		case BufferedImage.TYPE_INT_RGB:
			return 3;
		case BufferedImage.TYPE_CUSTOM:
			return 4;
		case BufferedImage.TYPE_4BYTE_ABGR_PRE:
			return 4;
		case BufferedImage.TYPE_INT_ARGB_PRE:
			return 4;
		case BufferedImage.TYPE_USHORT_555_RGB:
			return 3;
		case BufferedImage.TYPE_USHORT_565_RGB:
			return 3;
		case BufferedImage.TYPE_USHORT_GRAY:
			return 1;
		}
		return 0;
	}

	/**
	 *
	 * returns one row (height == 1) of byte packed image data in BGR or AGBR
	 * form
	 *
	 * @param img
	 * @param y
	 * @param w
	 * @param array
	 * @param temp
	 *            must be either null or a array with length of w*h
	 * @return
	 */
	public static byte[] getPixelsBGR(BufferedImage img, int y, int w,
			byte[] array, int[] temp) {
		final int x = 0;
		final int h = 1;
		assert array.length == temp.length * nrChannels(img);
		assert (temp.length == w);
		int imageType = img.getType();
		Raster raster;
		switch (imageType) {
		case BufferedImage.TYPE_3BYTE_BGR:
		case BufferedImage.TYPE_4BYTE_ABGR:
		case BufferedImage.TYPE_4BYTE_ABGR_PRE:
		case BufferedImage.TYPE_BYTE_GRAY:
			raster = img.getRaster();
			// int ttype= raster.getTransferType();
			raster.getDataElements(x, y, w, h, array);
			break;
		case BufferedImage.TYPE_INT_BGR:
			raster = img.getRaster();
			raster.getDataElements(x, y, w, h, temp);
			ints2bytes(temp, array, 0, 1, 2); // bgr --> bgr
			break;
		case BufferedImage.TYPE_INT_RGB:
			raster = img.getRaster();
			raster.getDataElements(x, y, w, h, temp);
			ints2bytes(temp, array, 2, 1, 0); // rgb --> bgr
			break;
		case BufferedImage.TYPE_INT_ARGB:
		case BufferedImage.TYPE_INT_ARGB_PRE:
			raster = img.getRaster();
			raster.getDataElements(x, y, w, h, temp);
			ints2bytes(temp, array, 2, 1, 0, 3); // argb --> abgr
			break;
		case BufferedImage.TYPE_CUSTOM: // TODO: works for my icon image loader,
										// but else ???
			img.getRGB(x, y, w, h, temp, 0, w);
			ints2bytes(temp, array, 2, 1, 0, 3); // argb --> abgr
			break;
		default:
			img.getRGB(x, y, w, h, temp, 0, w);
			ints2bytes(temp, array, 2, 1, 0); // rgb --> bgr
			break;
		}
		return array;
	}

	/**
	 * converts and copies byte packed BGR or ABGR into the img buffer, the img
	 * type may vary (e.g. RGB or BGR, int or byte packed) but the number of
	 * components (w/o alpha, w alpha, gray) must match
	 *
	 * does not unmange the image for all (A)RGN and (A)BGR and gray imaged
	 *
	 */
	public static void setBGRPixels(byte[] bgrPixels, BufferedImage img, int x,
			int y, int w, int h) {
		int imageType = img.getType();
		WritableRaster raster = img.getRaster();
		// int ttype= raster.getTransferType();
		if (imageType == BufferedImage.TYPE_3BYTE_BGR
				|| imageType == BufferedImage.TYPE_4BYTE_ABGR
				|| imageType == BufferedImage.TYPE_4BYTE_ABGR_PRE
				|| imageType == BufferedImage.TYPE_BYTE_GRAY) {
			raster.setDataElements(x, y, w, h, bgrPixels);
		} else {
			int[] pixels;
			if (imageType == BufferedImage.TYPE_INT_BGR) {
				pixels = bytes2int(bgrPixels, 2, 1, 0); // bgr --> bgr
			} else if (imageType == BufferedImage.TYPE_INT_ARGB
					|| imageType == BufferedImage.TYPE_INT_ARGB_PRE) {
				pixels = bytes2int(bgrPixels, 3, 0, 1, 2); // abgr --> argb
			} else {
				pixels = bytes2int(bgrPixels, 0, 1, 2); // bgr --> rgb
			}
			if (w == 0 || h == 0) {
				return;
			} else if (pixels.length < w * h) {
				throw new IllegalArgumentException(
						"pixels array must have a length" + " >= w*h");
			}
			if (imageType == BufferedImage.TYPE_INT_ARGB
					|| imageType == BufferedImage.TYPE_INT_RGB
					|| imageType == BufferedImage.TYPE_INT_ARGB_PRE
					|| imageType == BufferedImage.TYPE_INT_BGR) {
				raster.setDataElements(x, y, w, h, pixels);
			} else {
				// Unmanages the image
				img.setRGB(x, y, w, h, pixels, 0, w);
			}
		}
	}

	public static void ints2bytes(int[] in, byte[] out, int index1, int index2,
			int index3) {
		for (int i = 0; i < in.length; i++) {
			int index = i * 3;
			int value = in[i];
			out[index + index1] = (byte) value;
			value = value >> 8;
			out[index + index2] = (byte) value;
			value = value >> 8;
			out[index + index3] = (byte) value;
		}
	}

	public static void ints2bytes(int[] in, byte[] out, int index1, int index2,
			int index3, int index4) {
		for (int i = 0; i < in.length; i++) {
			int index = i * 4;
			int value = in[i];
			out[index + index1] = (byte) value;
			value = value >> 8;
			out[index + index2] = (byte) value;
			value = value >> 8;
			out[index + index3] = (byte) value;
			value = value >> 8;
			out[index + index4] = (byte) value;
		}
	}

	public static int[] bytes2int(byte[] in, int index1, int index2, int index3) {
		int[] out = new int[in.length / 3];
		for (int i = 0; i < out.length; i++) {
			int index = i * 3;
			int b1 = (in[index + index1] & 0xff) << 16;
			int b2 = (in[index + index2] & 0xff) << 8;
			int b3 = in[index + index3] & 0xff;
			out[i] = b1 | b2 | b3;
		}
		return out;
	}

	public static int[] bytes2int(byte[] in, int index1, int index2,
			int index3, int index4) {
		int[] out = new int[in.length / 4];
		for (int i = 0; i < out.length; i++) {
			int index = i * 4;
			int b1 = (in[index + index1] & 0xff) << 24;
			int b2 = (in[index + index2] & 0xff) << 16;
			int b3 = (in[index + index3] & 0xff) << 8;
			int b4 = in[index + index4] & 0xff;
			out[i] = b1 | b2 | b3 | b4;
		}
		return out;
	}

	public static BufferedImage convert(BufferedImage src, int bufImgType) {
		BufferedImage img = new BufferedImage(src.getWidth(), src.getHeight(),
				bufImgType);
		Graphics2D g2d = img.createGraphics();
		g2d.drawImage(src, 0, 0, null);
		g2d.dispose();
		return img;
	}

	/**
	 * Copy jpeg meta data (exif) from source to dest and save it to out.
	 *
	 * @param source
	 * @param dest
	 * @return result
	 * @throws IOException
	 */
	public static byte[] copyJpegMetaData(byte[] source, byte[] dest)
			throws IOException {
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		ImageOutputStream out = new MemoryCacheImageOutputStream(baos);
		copyJpegMetaData(new ByteArrayInputStream(source),
				new ByteArrayInputStream(dest), out);
		return baos.toByteArray();
	}

	/**
	 * Copy jpeg meta data (exif) from source to dest and save it to out
	 *
	 * @param source
	 * @param dest
	 * @param out
	 * @throws IOException
	 */
	public static void copyJpegMetaData(InputStream source, InputStream dest,
			ImageOutputStream out) throws IOException {
		// Read meta data from src image
		ImageReader reader = ImageIO.getImageReadersByFormatName("jpeg").next();
		ImageInputStream iis = new MemoryCacheImageInputStream(source);
		reader.setInput(iis);
		IIOMetadata metadata = reader.getImageMetadata(0);
		iis.close();
		// Read dest image
		ImageInputStream outIis = new MemoryCacheImageInputStream(dest);
		reader.setInput(outIis);
		IIOImage image = reader.readAll(0, null);
		image.setMetadata(metadata);
		outIis.close();
		// write dest image
		ImageWriter writer = ImageIO.getImageWritersByFormatName("jpeg").next();
		writer.setOutput(out);
		writer.write(image);
	}

  
    private static class MyRenderedImage implements RenderedImage {
        private final WritableRaster raster;
        private final ColorModel colorModel;

        public MyRenderedImage(WritableRaster raster, ColorModel colorModel) {
            this.raster = raster;
            this.colorModel = colorModel;
        }

        public Vector<RenderedImage> getSources() {
            return null;
        }

        public Object getProperty(String name) {
            return null;
        }

        public String[] getPropertyNames() {
            return new String[0];
        }

        public ColorModel getColorModel() {
            return colorModel;
        }

        public SampleModel getSampleModel() {
            return raster.getSampleModel();
        }

        public int getWidth() {
            return raster.getWidth();
        }

        public int getHeight() {
            return raster.getHeight();
        }

        public int getMinX() {
            return 0;
        }

        public int getMinY() {
            return 0;
        }

        public int getNumXTiles() {
            return 1;
        }

        public int getNumYTiles() {
            return 1;
        }

        public int getMinTileX() {
            return 0;
        }

        public int getMinTileY() {
            return 0;
        }

        public int getTileWidth() {
            return getWidth();
        }

        public int getTileHeight() {
            return getHeight();
        }

        public int getTileGridXOffset() {
            return 0;
        }

        public int getTileGridYOffset() {
            return 0;
        }

        public Raster getTile(int tileX, int tileY) {
            return raster;
        }

        public Raster getData() {
            return raster;
        }

        public Raster getData(Rectangle rect) {
            SampleModel sm = raster.getSampleModel();
            SampleModel nsm = sm.createCompatibleSampleModel(rect.width,
                                                             rect.height);
            WritableRaster wr = Raster.createWritableRaster(nsm,
                                                            rect.getLocation());
            int width = rect.width;
            int height = rect.height;
            int startX = rect.x;
            int startY = rect.y;

            return copyData(raster, startX, startY, width, height, wr);
        }

        public WritableRaster copyData(WritableRaster outRaster) {
            if (outRaster == null) {
                return (WritableRaster) getData();
            }
            int width = outRaster.getWidth();
            int height = outRaster.getHeight();
            int startX = outRaster.getMinX();
            int startY = outRaster.getMinY();

            return copyData(raster, startX, startY, width, height, outRaster);
        }

        private static WritableRaster copyData(WritableRaster raster,
                                               int startX, int startY,
                                               int width, int height,
                                               WritableRaster outRaster) {
            Object tdata = null;

            for (int i = startY; i < startY + height; i++) {
                tdata = raster.getDataElements(startX, i, width, 1, tdata);
                outRaster.setDataElements(startX, i, width, 1, tdata);
            }

            return outRaster;
        }
    }
}
